iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0
Modern Web

從Vue學React!不只要會用,還要真的懂~系列 第 23

【Day 23】利用useReducer + useContext管理複雜的狀態邏輯

  • 分享至 

  • xImage
  •  

我們在前幾天已經認識了useReducer和useContext的用法,今天來點進階的內容,把useReducer和useContext結合在一起使用看看。這兩個hook一起使用可以解決「當元件拆小後,需要往多層傳遞state的狀況」和「複雜操作不好管理」的問題,今天一樣透過實際的例子來了解要怎麼將他們結合來使用。

當專案越來越龐大,元件越拆越小...

為了讓多個頁面可以共用相同的元件,我們可能會把元件越拆越小,但是主要的邏輯或從api來的資料只會放在最上面的父層,那state或是有關state的操作就必須透過props層層往下傳遞到要使用的元件。這麼做沒有不行,的確也是一個方式,但是如果需要調整邏輯或做拆分的動作,在這樣的使用方式下,可能就比較不好進行。

這裡直接來看一個類似這樣的情境。有一個頁面被拆分成三層,主要資料和state操作的函式都在父層。

// 這是父層
import { useState } from 'react';
import List from './List';
function App() {
  // 主要使用的state
  const [ listData, setListData ] = useState([
    {
      id: 1,
      title: 'item 1',
      img: 'https://picsum.photos/500/400?random=2',
      isFavorite: false,
    },
    {
      id: 2,
      title: 'item 2',
      img: 'https://picsum.photos/500/400?random=22',
      isFavorite: false,
    },
    {
      id: 3,
      title: 'item 3',
      img: 'https://picsum.photos/500/400?random=41',
      isFavorite: false,
    },
    {
      id: 4,
      title: 'item 4',
      img: 'https://picsum.photos/500/400?random=15',
      isFavorite: false,
    },
    {
      id: 5,
      title: 'item 5',
      img: 'https://picsum.photos/500/400?random=77',
      isFavorite: false,
    }
  ]);
  // 操作state的函式(新增最愛的item)
  const addFavoriteItem = (id) => {
    const updatedListData = listData.map((item) => {
      if (item.id === id) {
        return {
          ...item,
          isFavorite: !item.isFavorite
        }
      }
      return item;
    })
    setListData(updatedListData)
  };
  // 操作state的函式(移除item)
  const deleteItem = (id) => {
    const updatedListData = listData.filter((item) => item.id !== id)
    setListData(updatedListData)
  };
  return (
    <div className="App">
      // 需要個別把state和兩個針對state操作的動作往下傳
      <List listData={listData} addFavoriteItem={addFavoriteItem} deleteItem={deleteItem} />
    </div>
  );
}

再來是父層裡面的List元件

// 父層底下的List子元件
import ListItem from "./ListItem";
export default function List({listData, addFavoriteItem, deleteItem}) {
  return (
    <div className="list-container">
      {
        listData.map((item) => (
          <ListItem key={item.id} item={item} addFavoriteItem={addFavoriteItem} deleteItem={deleteItem} />
        ))
      }
    </div>
  )
}

接著是List裡面的Item元件

// List元件底下的Item子元件
import Button from './Button';
export default function ListItem({item, addFavoriteItem, deleteItem}) {
  const handleAddFavoriteItem = () => {
    addFavoriteItem(item.id)
  };
  const handleDeleteItem = () => {
    deleteItem(item.id)
  }; 
  return (
    <div className={`list-item-container ${ item.isFavorite && 'favorite' }`} >
      <h2>{item.title}</h2>
      <img src={item.img} alt="" />
      <Button itemId={item.id} onClick={handleAddFavoriteItem}>Add favorite</Button>
      <Button itemId={item.id} onClick={handleDeleteItem}>Delete item</Button>
    </div>
  )
}

往下還有一個Item元件內的Button元件

export default function Button({onClick, children}) {
  return (
    <button onClick={onClick}>{children}</button>
  )
}

以上的這些程式碼就完成了可以選取成我的最愛,也可以將item刪除的功能。
https://i.imgur.com/kk9XiXP.gif

在這個小範例中,會遇到兩個問題,分別是「需要往下傳的props很多」,以及「需要往下傳的層數很多」。
這還只是一個不算太複雜的結構,但是因為當前的元件切分結構,如果要處理點擊按鈕的動作,就需要把用來處理點擊動作所延伸來更新state的函式往下傳兩層。當日後這樣的結構越來越龐大,功能越來越複雜,也就會讓把多個state和函式往下傳多層的這個做法,變得很不方便。

如果把state的操作動作管理在一起

現在還只有兩個操作state的動作,要把它們往下傳遞,就已經要多寫很多props了,如果今天還有更多操作動作的話,需要往下傳的函式就會變得更多。這時候其實就可以考慮把操作state的動作都統一管理,統一往下傳遞,減少需要往下傳遞的props數量,讓整體程式碼變得更簡潔。

父層App
這裡改成使用useReducer管理state,當要傳遞操作state的動作下去時,就可以只傳遞透過useReducer回傳的dispatch。

import { useReducer } from 'react';
import List from './List';

const initialState = [
  {
    id: 1,
    title: 'item 1',
    img: 'https://picsum.photos/500/400?random=2',
    isFavorite: false,
  },
  // ... 略
];

// 改成把state和操作state的動作都統一用reducer管理在一起
const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_FAVORITE_ITEM':
      return state.map(item =>
        item.id === action.payload
          ? { ...item, isFavorite: !item.isFavorite }
          : item
      );
    case 'DELETE_ITEM':
      return state.filter(item =>
        item.id !== action.payload,
      );
    default:
      return state;
  }
};


function App() {
  // 使用useReducer取代useState
  const [listData, dispatch] = useReducer(reducer, initialState);

  return (
    <div className="App">
      // 改成把dispatch這個把所有state操作動作都統整起來的函式往下傳
      <List listData={listData} dispatch={dispatch} />
    </div>
  );
}

export default App;

再來是父層裡面的List子元件
繼續把dispatch往下傳

import ListItem from "./ListItem";

export default function List({listData, dispatch}) {
  return (
    <div className="list-container">
      {
        listData.map((item) => (
          <ListItem key={item.id} item={item} dispatch={dispatch} />
        ))
      }
    </div>
  )
}

接著是List裡面的Item元件
這裡是真正觸發state操作的地方,一樣變成統一使用dispatch,只要用action type告訴dispatch要進行哪個動作就好。這樣調整之後,針對state的操作就變得更一目瞭然。

export default function ListItem({item, dispatch}) {
  const handleAddFavoriteItem = () => {
    dispatch({ type: 'ADD_FAVORITE_ITEM', payload: item.id })
  }; 
  const handleDeleteItem = () => {
    dispatch({ type: 'DELETE_ITEM', payload: item.id })
  }; 
  return (
    <div className={`list-item-container ${ item.isFavorite && 'favorite' }`} >
      <h2>{item.title}</h2>
      <img src={item.img} alt="" />
      <Button itemId={item.id} onClick={handleAddFavoriteItem}>Add favorite</Button>
      <Button itemId={item.id} onClick={handleDeleteItem}>Delete item</Button>
    </div>
  )
}

如果再把資料透過context往下傳

經過前面的調整後,雖然減少一個需要往子層傳的props了,但是還是沒改善需要把state層層傳遞的部分。沒錯!這時候useContext就可以派上用場了。

使用useContext的第一步-用createContext創建context

創建一個名為ListDataContext的檔案,裡面會有兩個context,分別是item資料的context改動state的dispatch context

import { createContext } from 'react';

export const ListDataContext = createContext(null);
export const ListDataDispatchContext = createContext(null);

把provider包在父層內的最外層,把要往下傳的state和函式帶上

把前面創建好的context import進父層,並且拿context的provider包在最外層,並用value把要傳下去的listData和dispatch帶上。
因為需要把兩個context都帶上,所以會在使用的父層包兩個provider


import { ListDataContext, ListDataDispatchContext } from './ListDataContext';

// 中間略

function App() {
  const [listData, listDataDispatch] = useReducer(reducer, initialState);

  return (
    <div className="App">
      // 把useReducer的state和dispatch透過value帶上
      <ListDataContext.Provider value={listData}>
        <ListDataDispatchContext.Provider value={listDataDispatch}>
          <List />
        </ListDataDispatchContext.Provider>
      </ListDataContext.Provider>
    </div>
  );
}

使用useContext把要使用的state和函式取出來使用

接下來就可以透過useContext,取得當前元件要使用的listData或dispatch,並且可以把原本用props傳遞的部分拿掉。

import ListItem from "./ListItem";
import { useContext } from "react";
import { ListDataContext } from "./ListDataContext";

export default function List() {
  const listData = useContext(ListDataContext);
  return (
    <div className="list-container">
      {
        listData.map((item) => (
          <ListItem key={item.id} item={item} />
        ))
      }
    </div>
  )
}
import Button from './Button';
import { useContext } from 'react';
import { ListDataDispatchContext } from './ListDataContext';

export default function ListItem({item}) {
  const dispatch = useContext(ListDataDispatchContext);
  const handleAddFavoriteItem = () => {
    dispatch({ type: 'ADD_FAVORITE_ITEM', payload: item.id })
  }; 
  const handleDeleteItem = () => {
    dispatch({ type: 'DELETE_ITEM', payload: item.id })
  }; 
  return (
    <div className={`list-item-container ${ item.isFavorite && 'favorite' }`} >
      <h2>{item.title}</h2>
      <img src={item.img} alt="" />
      <Button itemId={item.id} onClick={handleAddFavoriteItem}>Add favorite</Button>
      <Button itemId={item.id} onClick={handleDeleteItem}>Delete item</Button>
    </div>
  )
}

現在寫useReducer和useContext的方式分開的,如果想要讓程式碼更好維護,也可以進一步把useReducer的部分和useContext的部分歸類在同一個檔案中。

比較程式碼調整前和調整後,就可以發現調整後的程式碼可以擺脫層層傳遞資料的困擾,也能讓相關的資料都被放在同一處管理,變得更好維護。今天的內容雖然偏實作,但從實作的過程,也能對useReducer和useContext增加更多的熟悉度。到目前為止我們已經從渲染機制,到實作中常會用到的一些hooks來從Vue學React,接下來會進一步認識Vue和React本身以外的全域狀態管理Libary。

參考資料

Scaling Up with Reducer and Context


上一篇
【Day 22】 深層傳遞state!除了props還有其他方式 - proivde & inject和useContext
下一篇
【Day 24】跨越直系與旁系元件的全域狀態管理工具
系列文
從Vue學React!不只要會用,還要真的懂~30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言